#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "fastcgi.h"
#include "http_server.h"
#include "read_buffer.h"
#include "methods.h"

static int prepare_header (buffer_t *buffer, size_t offset, unsigned char type, unsigned short requestId, size_t content_length, unsigned char padding_length);
static int add_parameter (buffer_t *buffer, const char *name, size_t namelen, const char *value, size_t valuelen);

int fastcgi_prepare_begin_request (buffer_t *buffer, unsigned short requestId)
{
	FCGI_BeginRequestBody *begin_request_body;
	size_t offset;

	if (buffer_allocate (buffer, sizeof (FCGI_Header) + sizeof (FCGI_BeginRequestBody)) < 0) {
		return -1;
	}

	offset = buffer->used;
	if (prepare_header (buffer, offset, FCGI_BEGIN_REQUEST, requestId, sizeof (FCGI_BeginRequestBody), 0) < 0) {
		return -1;
	}

	begin_request_body = (FCGI_BeginRequestBody *) (buffer->data + offset + sizeof (FCGI_Header));

	begin_request_body->roleB1 = 0;
	begin_request_body->roleB0 = FCGI_RESPONDER;
	begin_request_body->flags = 0;

	memset (begin_request_body->reserved, 0, sizeof (begin_request_body->reserved));

	buffer->used += (sizeof (FCGI_Header) + sizeof (FCGI_BeginRequestBody));

	return 0;
}

int fastcgi_prepare_abort_request (buffer_t *buffer, unsigned short requestId)
{
	if (buffer_allocate (buffer, sizeof (FCGI_Header)) < 0) {
		return -1;
	}

	if (prepare_header (buffer, buffer->used, FCGI_ABORT_REQUEST, requestId, 0, 0) < 0) {
		return -1;
	}

	buffer->used += sizeof (FCGI_Header);

	return 0;
}

int fastcgi_prepare_parameter_list (buffer_t *buffer, unsigned short requestId, http_connection_t *http_connection, const char *query, size_t query_len)
{
	host_t *host;
	size_t offset;
	size_t size;
	char filename[PATH_MAX];
	char string[128];
	int len;
	size_t remainder;
	size_t diff;
	size_t pathlen;

	if ((host = host_list_search_host (&http_server.host_list, http_connection->host.data)) == NULL) {
		return -1;
	}

	if (buffer_allocate (buffer, sizeof (FCGI_Header)) < 0) {
		return -1;
	}

	offset = buffer->used;
	buffer->used += sizeof (FCGI_Header);

	if (add_parameter (buffer, "SERVER_NAME", 11, http_connection->host.data, http_connection->host.used - 1) < 0) {
		return -1;
	}

	len = snprintf (string, sizeof (string), "%d", http_server.port);
	if (add_parameter (buffer, "SERVER_PORT", 11, string, len) < 0) {
		return -1;
	}

	if (add_parameter (buffer, "REQUEST_METHOD", 14, methods[http_connection->http_method].string, methods[http_connection->http_method].len) < 0) {
		return -1;
	}

	if (!inet_ntop (AF_INET, &(http_connection->sin.sin_addr), string, sizeof (string))) {
		return -1;
	}

	len = strlen (string);
	if (add_parameter (buffer, "REMOTE_ADDR", 11, string, len) < 0) {
		return -1;
	}

	if (add_parameter (buffer, "DOCUMENT_ROOT", 13, host->document_root, host->document_root_length) < 0) {
		return -1;
	}

	if (http_connection->query < 0) {
		pathlen = http_connection->path.used - 1;
	} else {
		pathlen = http_connection->query;
	}

	if (add_parameter (buffer, "REQUEST_URI", 11, http_connection->path.data, pathlen) < 0) {
		return -1;
	}

	if (add_parameter (buffer, "PATH_INFO", 9, http_connection->path.data, pathlen) < 0) {
		return -1;
	}

	len = snprintf (filename, sizeof (filename), "%s%.*s", host->document_root, pathlen, http_connection->path.data);
	if (add_parameter (buffer, "SCRIPT_FILENAME", 15, filename, len) < 0) {
		return -1;
	}

	if (add_parameter (buffer, "SCRIPT_NAME", 11, http_connection->path.data, pathlen) < 0) {
		return -1;
	}

	if (http_connection->user_agent.used > 0) {
		if (add_parameter (buffer, "HTTP_USER_AGENT", 15, http_connection->user_agent.data, http_connection->user_agent.used) < 0) {
			return -1;
		}
	}

	if (add_parameter (buffer, "SERVER_PROTOCOL", 15, "HTTP/1.1", 8) < 0) {
		return -1;
	}

	if (http_connection->keep_alive) {
		if (add_parameter (buffer, "HTTP_CONNECTION", 15, "Keep-Alive", 10) < 0) {
			return -1;
		}
	} else {
		if (add_parameter (buffer, "HTTP_CONNECTION", 15, "close", 5) < 0) {
			return -1;
		}
	}

	if (add_parameter (buffer, "GATEWAY_INTERFACE", 17, "CGI/1.1", 7) < 0) {
		return -1;
	}

	if (add_parameter (buffer, "QUERY_STRING", 12, query, query_len) < 0) {
		return -1;
	}

	if (http_connection->content_type.used > 0) {
		if (add_parameter (buffer, "CONTENT_TYPE", 12, http_connection->content_type.data, http_connection->content_type.used) < 0) {
			return -1;
		}
	}

	if (http_connection->content_length > 0) {
		len = snprintf (string, sizeof (string), "%d", http_connection->content_length);
		if (add_parameter (buffer, "CONTENT_LENGTH", 14, string, len) < 0) {
			return -1;
		}
	}

	/* Add padding (if necessary). */
	size = buffer->used - (offset + sizeof (FCGI_Header));

	remainder = size % 8;
	if (remainder != 0) {
		diff = 8 - remainder;
		if (buffer_allocate (buffer, diff) < 0) {
			return -1;
		}

		memset (buffer->data + buffer->used, 0, diff);

		buffer->used += diff;
	} else {
		diff = 0;
	}

	if (prepare_header (buffer, offset, FCGI_PARAMS, requestId, size, diff) < 0) {
		return -1;
	}

	if (buffer_allocate (buffer, sizeof (FCGI_Header)) < 0) {
		return -1;
	}

	offset = buffer->used;
	buffer->used += sizeof (FCGI_Header);
	if (prepare_header (buffer, offset, FCGI_PARAMS, requestId, 0, 0) < 0) {
		return -1;
	}

	return 0;
}

int fastcgi_prepare_data (buffer_t *buffer, unsigned short requestId, const char *data, size_t len)
{
	size_t remainder;
	size_t diff;

	remainder = len % 8;
	if (remainder != 0) {
		diff = 8 - remainder;
	} else {
		diff = 0;
	}

	if (buffer_allocate (buffer, sizeof (FCGI_Header) + len + diff) < 0) {
		return -1;
	}

	if (prepare_header (buffer, buffer->used, FCGI_STDIN, requestId, len, diff) < 0) {
		return -1;
	}

	buffer->used += sizeof (FCGI_Header);

	memcpy (buffer->data + buffer->used, data, len);
	buffer->used += len;

	/* Add padding (if necessary). */
	if (diff > 0) {
		memset (buffer->data + buffer->used, 0, diff);

		buffer->used += diff;
	}

	return 0;
}

int prepare_header (buffer_t *buffer, size_t offset, unsigned char type, unsigned short requestId, size_t content_length, unsigned char padding_length)
{
	FCGI_Header *header;

	if (content_length >= 64 * 1024) {
		return -1;
	}

	header = (FCGI_Header *) (buffer->data + offset);

	header->version = FCGI_VERSION_1;
	header->type = type;
	header->requestIdB1 = (requestId >> 8) & 0xff;
	header->requestIdB0 = requestId & 0x00ff;
	header->contentLengthB1 = (content_length >> 8) & 0xff;
	header->contentLengthB0 = content_length & 0x00ff;
	header->paddingLength = padding_length;

	header->reserved = 0;

	return 0;
}

int add_parameter (buffer_t *buffer, const char *name, size_t namelen, const char *value, size_t valuelen)
{
	size_t size;
	size_t offset;
	char *data;

	if ((namelen >= 0x80000000) || (valuelen >= 0x80000000)) {
		return -1;
	}

	if (namelen > 127) {
		size = 4;
	} else {
		size = 1;
	}

	if (valuelen > 127) {
		size += 4;
	} else {
		size += 1;
	}

	size += (namelen + valuelen);

	if (buffer_allocate (buffer, size) < 0) {
		return -1;
	}

	data = buffer->data;
	offset = buffer->used;

	if (namelen > 127) {
		data[offset++] = (0x80) | (namelen >> 24);
		data[offset++] = (namelen >> 16) & 0x000000ff;
		data[offset++] = (namelen >> 8) & 0x000000ff;
		data[offset++] = namelen & 0x000000ff;
	} else {
		data[offset++] = namelen;
	}

	if (valuelen > 127) {
		data[offset++] = (0x80) | (valuelen >> 24);
		data[offset++] = (valuelen >> 16) & 0x000000ff;
		data[offset++] = (valuelen >> 8) & 0x000000ff;
		data[offset++] = valuelen & 0x000000ff;
	} else {
		data[offset++] = valuelen;
	}

	memcpy (data + offset, name, namelen);
	offset += namelen;
	memcpy (data + offset, value, valuelen);
	offset += valuelen;

	buffer->used = offset;

	return 0;
}

int fastcgi_read_stdout_record (http_connection_t *http_connection)
{
	buffer_t *buffer = &http_connection->buffer;
	FCGI_Header *header;
	int ret;
	size_t content_length;
	size_t record_size;
	size_t used;
	size_t remaining;
	unsigned char padding;

	used = 0;

	do {
		/* If we don't have any data from a previous read operation... */
		if (http_connection->size_next_record == 0) {
			if (buffer_allocate (buffer, buffer->buffer_increment) < 0) {
				return -1;
			}

			/* Save how many bytes we have already. */
			if (http_connection->fastcgi_state == 0) {
				used = buffer->used;
			}

			if ((ret = read_buffer (http_connection)) < 0) {
				return -1;
			} else if (ret == 0) {
				if (http_connection->fastcgi_state == 0) {
					http_connection->size_next_record = buffer->used - used;
				} else if (http_connection->fastcgi_state == 1) {
					http_connection->size_next_record = (sizeof (FCGI_Header) + http_connection->record_content_length + http_connection->padding) - http_connection->pending_current_record;
				}

				return 0;
			}
		} else {
			ret = http_connection->size_next_record;
			used = buffer->used - ret;

			http_connection->size_next_record = 0;
		}

		do {
			if (http_connection->fastcgi_state == 0) {
				/* If we haven't read the whole FastCGI header... */
				if (ret < sizeof (FCGI_Header)) {
					break; /* Read again. */
				}

				header = (FCGI_Header *) (buffer->data + used);

				if (header->type == FCGI_END_REQUEST) {
					buffer->used -= ret;
					return -1;
				}

				content_length = (header->contentLengthB1 << 8) + header->contentLengthB0;
				if (content_length == 0) {
					buffer->used -= ret;
					return -1;
				}

				padding = header->paddingLength;

				record_size = sizeof (FCGI_Header) + content_length + padding;

				if (header->type == FCGI_STDERR) {
					if (ret < record_size) {
						http_connection->pending_current_record = record_size - ret;
						buffer->used = used;

						http_connection->fastcgi_state = 2;
						break; /* Read again. */
					}

					remaining = ret - record_size;
					if (remaining == 0) {
						buffer->used = used;
						break; /* Read again. */
					}

					memmove (buffer->data + used, buffer->data + used + record_size, remaining);
					buffer->used -= record_size;

					ret = remaining;

					continue;
				} else if (header->type != FCGI_STDOUT) {
					return -1;
				}

				http_connection->record_content_length = content_length;

				memmove (buffer->data + used, buffer->data + used + sizeof (FCGI_Header), ret - sizeof (FCGI_Header));
				buffer->used -= sizeof (FCGI_Header);

				/* If we haven't read the whole content length... */
				if (ret < record_size) {
					http_connection->pending_current_record = record_size - ret;
					http_connection->padding = padding;

					http_connection->fastcgi_state = 1;
					break; /* Read again. */
				}

				http_connection->size_next_record = ret - record_size;

				if (padding > 0) {
					if (http_connection->size_next_record > 0) {
						memmove (buffer->data + used + content_length, buffer->data + used + content_length + padding, http_connection->size_next_record);
					}

					buffer->used -= padding;
				}

				return content_length;
			} else if (http_connection->fastcgi_state == 1) {
				/* Reading stdout. */
				if (ret < http_connection->pending_current_record) {
					http_connection->pending_current_record -= ret;
					break; /* Read again. */
				}

				http_connection->size_next_record = ret - http_connection->pending_current_record;

				if (http_connection->padding > 0) {
					if (http_connection->size_next_record > 0) {
						memmove (buffer->data + used + http_connection->record_content_length, buffer->data + used + http_connection->record_content_length + http_connection->padding, http_connection->size_next_record);
					}

					buffer->used -= http_connection->padding;
				}

				http_connection->pending_current_record = 0;

				http_connection->fastcgi_state = 0;

				return http_connection->record_content_length;
			} else {
				/* Reading stderr. */
				if (ret < http_connection->pending_current_record) {
					http_connection->pending_current_record -= ret;
					buffer->used = used;
					break; /* Read again. */
				}

				remaining = ret - http_connection->pending_current_record;
				http_connection->fastcgi_state = 0;

				if (remaining == 0) {
					http_connection->pending_current_record = 0;
					buffer->used = used;
					break; /* Read again. */
				}

				memmove (buffer->data + used, buffer->data + used + http_connection->pending_current_record, remaining);
				buffer->used -= http_connection->pending_current_record;

				http_connection->pending_current_record = 0;

				ret = remaining;
			}
		} while (1);
	} while (1);
}
